【高可用】必须修炼《流控》这把利剑!
国庆期间,小胖约上女朋友去上海迪士尼游玩,创极速光轮
、翱翔•飞越地平线
、漫威英雄总部
、小熊维尼历险记
、爱丽丝梦游仙境迷宫
等好多童年回忆,一定可以玩的畅快淋漓。
可是到了现场,却发现人山人海,目惊口呆,刹那间好像全国的宅男们都和小胖想到一块去了。
面对这么热情的游客,迪士尼要怎么接待,如果放开闸口,游客一拥而上,大家都玩不好。
这时候就需要有些应对措施,比如一天限量3万张门票
,并在入口检票处、各个游玩项目入口处都有排队机制,控制人数,维持秩序,保证大家都能玩的开心。
其实,不止是线下,线上也有流控。每年的春运抢火车票、天猫双十一购物狂欢节、618大促、秒杀活动、各种电商促销活动,这些都会带来瞬间高访问流量,如果不做有效控制,很容易把系统冲垮,影响用户体验,甚至引发各种资损。
这也是我们要做 《流控》 的原因,保护核心系统的可用性!
🌴 什么是限流
限流定义:
限制到达系统的并发请求数量,保证系统能够正常响应部分用户请求,而对于超过限制的流量,则通过拒绝服务的方式保证整体系统的可用性。
说白了,限流
就是敢于说不
。所以,为了保证效果,尽量将限流组件
前置化,降低无效流量
对系统的损耗。比如,部署在API网关
中,统一处理;当然如果担心分发下来的流量还是很大的话,下游的微服务系统内部也可以引入限流组件
,多层保障~~
根据作用范围:限流分为单机版限流、分布式限流。
1、单机版限流
主要借助于本机内存来实现计数器,比如通过AtomicLong#incrementAndGet()
,但是要注意之前不用的key定期做清理,释放内存。
纯内存实现,无需和其他节点统计汇总,性能最高。但是优点也是缺点,无法做到全局统一化
的限流。
2、分布式限流
单机版限流仅能保护自身节点,但无法保护应用依赖的各种服务,并且在进行节点扩容、缩容时也无法准确控制整个服务的请求限制。而分布式限流,以集群为维度,可以方便的控制这个集群的请求限制,从而保护下游依赖的各种服务资源。
限流支持多个维度:
整个系统一定时间内(比如每分钟)处理多少请求 单个接口一定时间内处理多少流量 单个IP、城市、渠道、设备id、用户id等在一定时间内发送的请求数 如果是开放平台,则为每个 appkey
设置独立的访问速率规则
限流维度更多体现了业务侧需求,实现原理类似,都是抽取对应的关键key数据,按照一定的限流算法来控制访问频率。
那具体常见的限流算法有哪些?
🌴 计数器限流
限制一段时间内发向系统的总体请求量,算法实现比较简单。
每次接收到了请求 ,内存中给计数器加1,并将最新值与阈值比较,低于设置的阈值则正常处理改请求,否则拒绝本次服务。
实现起来比较简单,但是有一个致命缺陷,临界问题
如上图所示,对于一个业务接口配置限流规则,每5秒钟处理请求不能超过 1000 假如在第【4,5】秒间,涌进来一个900个请求,但【0,5】秒钟内的总数小于1000,所以系统照单全收 然后过了5秒这个时刻点,进入【5,10】秒这个周期,新周期会重新计数 不凑巧的是,在【5,6】秒之间,又涌进来一大波请求,有900多个,由于此时的计数还没超过1000,限流不会拦截 这个时候【4,6】秒间,系统大约接收了 1800个请求,严重超过了系统的承载能力。 虽然也配置了 计数器限流
,但是用户利用临界点
这个漏洞,瞬间压垮我们的系统
为了解决这个缺陷,就有了下面要介绍的基于滑动窗口的算法
。
🌴 滑动窗口限流
业务的限流规则没有变,还是每5秒钟处理请求不能超过 1000
与 计数器限流
的区别在于,将5秒钟分成了5个1秒
的小块,每个1秒
小块独立计数当在第5秒钟接收到一个请求时,会将第1秒到第5秒间的5个格子的计数求和,然后与设置的阈值比较,此时是940,如果没有超过1000,限流不会拦截 当在第6秒的时候,接收到一个请求,则需将第2秒到第6秒间的5个格子的计数求和,此时是2100,限流生效。 计数器限流
的问题能够解决。
滑动窗口限流可以有效解决这个精度问题。当滑动窗口的格子数拆分的越多,那么滑动窗口的滚动就越平滑,限流的统计就越精确。
但是也不是窗口拆分的越多越好,空间复杂度以及统计计数开销都会大大增加
,就像这世间万物都存在相生相克,没有什么是绝对好,只要合适就好。
滑动窗口应用广泛,除了在业务系统中大量使用,在TCP协议
中也有引入。
TCP滑动窗口
是在传输层进行流控的一种措施,分为发送窗口
、接收窗口
。接收方通过数据包发送方自己当前窗口大小
和期望接收到的下一字节的序号n
,从而控制发送方的发送速度,实现对网络传输流量的控制,防止发送方速度过快导致自己被淹没。
虽然滑动窗口限流解决了窗口边界的大流量问题,但是它和计数器限流
类似,无法限制短时间之内的集中流量,也就是说无法控制流量让它们更加平滑。那么有没有解呢?答案是肯定的,接下来我们来介绍漏桶限流
🌴 漏桶限流
漏桶限流主要是在流量生产端
和接收端
之间增加了一个漏桶,流量会先暂存在漏桶中,如果流入的流量短时间内暴增,超过了漏桶的部分会被拒绝服务,而漏桶的出口处会按照一个固定的速率将流量漏出给消费端
,进行各种业务处理。
经过了漏桶限流之后,随机产生的流量就会成为比较平滑的流量
到达服务端,削峰填谷
,从而避免了突发的大流量对于服务接口的影响。
技术方案实现,我们一般使用消息队列
作为漏桶的实现。
漏桶限流,虽然能应付集中高并发流量冲击,但始终恒定的处理速度(严重依赖于消费端的消费速度)有时候并不一定是件好事,作为互联网应用,我们应该尽最大努力去快速响应用户请求,无论是处理还是拒绝。
响应时间真的很重要,太久会严重影响用户体验,引发用户流失。另外,面对流量突增时调整起来不够方便,这时候,我们可以考虑令牌桶限流
🌴 令牌桶限流
令牌桶限流,既能达到精确限流的作用,还能快速给用户响应。
基本流程:
如果限流规则是1秒钟1000次请求,那么每隔1毫秒,流量生产端会往桶中放入一个令牌 接收端在处理一个请求之前,先从桶中取一个令牌,才能继续后续的业务处理。如果桶中没有令牌,则需要等待或者拒绝服务 初始化时, 会限制桶中的令牌总数
,如果超过了限制数
就不再向桶中放入新的令牌。
令牌桶算法生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。意味着,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。虽然瞬间拿走了桶中的令牌,但由于流量生产端
生产流量的速度是恒定的
,后续的请求会被拦截,不会冲击到系统。
Google的Guava
包中的RateLimiter
类就是令牌桶算法的解决方案,不过只适用于单机版。
如果是分布式环境,我们可以将令牌存储在Redis
中(incr 计数),每次处理请求前访问一次Redis
获取一个令牌,但是会有性能损耗(约1~2ms)。也可以进一步优化,采用批量拉取方式,一次获取多个令牌,放在client端临时存储,待本地的令牌使用完后,再从令牌桶
中申请。
在实际项目中,令牌桶限流使用较多,我们可以基于当前系统的服务能力动态调整令牌的发放速率,此外我们还可以动态调整桶大小(或者为令牌设置生命周期),以防止大量令牌累积导致的“伪限流失效”现象。
🌴 写在最后
限流的难点在于配置 ,不管是静态配置还是动态配置,如何让限流在不误伤的前提下尽量发挥最大作用是一个富有经验的问题,如何找到这个平衡点,压测是一个基础且行之有效的方法。
推荐阅读
码字不易,请不要白嫖。如果对您的工作有帮助,请转发分享,点个 “赞”